use core::ptr::null_mut;

use libc::{
    MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED_NOREPLACE, MAP_PRIVATE, PROT_READ, PROT_WRITE,
    _SC_PAGESIZE,
};

use crate::{
    stats::{mi_stat_decrease, mi_stat_increase},
    types::{mi_os_tld_t, mi_stats_t, MI_SEGMENT_SIZE},
};

pub fn _mi_align_up(sz: usize, alignment: usize) -> usize {
    let mut x = (sz / alignment) * alignment;
    if x < sz {
        x += alignment;
    }

    if x < sz {
        return 0;
    }

    x
}

pub unsafe fn mi_align_up_ptr(p: *mut u8, alignment: usize) -> *mut u8 {
    _mi_align_up(p as usize, alignment) as _
}

pub fn _mi_align_down(sz: usize, alignment: usize) -> usize {
    (sz / alignment) * alignment
}

pub unsafe fn mi_align_down_ptr(p: *mut u8, alignment: usize) -> *mut u8 {
    _mi_align_down(p as usize, alignment) as _
}

pub unsafe fn _mi_os_alloc(size: usize, stats: *mut mi_stats_t) -> *mut u8 {
    if size == 0 {
        return null_mut();
    }

    let p = mi_mmap(null_mut(), size, 0, stats);
    if !p.is_null() {
        mi_stat_increase(&mut (&mut *stats).reserved, size);
    }
    p
}

pub unsafe fn _mi_os_free(p: *mut u8, size: usize, stats: *mut mi_stats_t) {
    mi_munmap(p, size);
    mi_stat_decrease(&mut (&mut *stats).reserved, size);
}

pub unsafe fn mi_munmap(addr: *mut u8, size: usize) -> bool {
    if addr.is_null() || size == 0 {
        return true;
    }

    let err = libc::munmap(addr as _, size);
    err == 0
}

pub unsafe fn mi_mmap(
    addr: *mut u8,
    size: usize,
    extra_flags: i32,
    stats: *mut mi_stats_t,
) -> *mut u8 {
    if size == 0 {
        return null_mut();
    }
    let mut flags = MAP_PRIVATE | MAP_ANONYMOUS | extra_flags;
    if !addr.is_null() {
        flags |= MAP_FIXED_NOREPLACE;
    }

    let pflags = PROT_READ | PROT_WRITE;
    let mut p = libc::mmap(addr as _, size, pflags, flags, -1, 0);
    if p == MAP_FAILED {
        p = null_mut();
    }

    if !addr.is_null() && p != addr as *mut _ {
        mi_munmap(p as *mut _, size);
        p = null_mut();
    }

    assert!(
        p.is_null()
            || (addr.is_null() && p != addr as *mut _)
            || (!addr.is_null() && p == addr as *mut _)
    );
    if !p.is_null() {
        mi_stat_increase(&mut (&mut *stats).mmap_calls, 1);
    }
    p as _
}

pub unsafe fn _mi_os_page_size() -> usize {
    static mut PAGE_SIZE: usize = 0;
    let out = libc::sysconf(_SC_PAGESIZE);

    if PAGE_SIZE == 0 {
        PAGE_SIZE = if out > 0 { out as usize } else { 4096 }
    }
    PAGE_SIZE
}

pub unsafe fn mi_os_page_align_region(
    addr: *mut u8,
    size: usize,
    mut new_size: Option<&mut usize>,
) -> *mut u8 {
    assert!(!addr.is_null() && size > 0);
    if let Some(new_size) = &mut new_size {
        **new_size = 0
    };
    if size == 0 || addr.is_null() {
        return null_mut();
    }

    let start = mi_align_up_ptr(addr, _mi_os_page_size());
    let end = mi_align_down_ptr(addr.add(size), _mi_os_page_size());
    let diff = end as i64 - start as i64;
    if diff <= 0 {
        return null_mut();
    }
    debug_assert!(diff as usize <= size);

    if let Some(new_size) = new_size {
        *new_size = diff as usize
    };
    start
}

pub unsafe fn _mi_os_shrink(p: *mut u8, old_size: usize, new_size: usize) -> bool {
    debug_assert!(old_size > new_size && !p.is_null());
    if old_size < new_size || p.is_null() {
        return false;
    }
    if old_size == new_size {
        return true;
    }

    let addr = p.add(new_size);
    let mut size = 0;
    let start = mi_os_page_align_region(addr, old_size - new_size, Some(&mut size));
    if size == 0 || start != addr {
        return false;
    }
    mi_munmap(start, size)
}

pub unsafe fn _mi_os_alloc_aligned(
    size: usize,
    alignment: usize,
    tld: *mut mi_os_tld_t,
) -> *mut u8 {
    if size == 0 {
        return null_mut();
    }

    if alignment < 1024 {
        return _mi_os_alloc(size, (&mut *tld).stats);
    }

    let mut p: *mut u8 = os_pool_alloc(size, alignment, tld);
    if !p.is_null() {
        return p;
    }

    let suggest = null_mut();
    if p.is_null() && (&*tld).mmap_next_probable % alignment == 0 {
        p = mi_mmap(suggest, size, 0, (&mut *tld).stats);
        if p.is_null() {
            return null_mut();
        }
        if p as usize % alignment == 0 {
            mi_stat_increase(&mut (&mut *(&mut *tld).stats).mmap_right_align, 1);
        }
    }

    if p.is_null() || (&*tld).mmap_next_probable % alignment != 0 {
        if !p.is_null() {
            mi_munmap(p, size);
        }
        mi_stat_increase(&mut (&mut *(&mut *tld).stats).mmap_ensure_aligned, 1);
        p = mi_os_alloc_aligned_ensured(size, alignment, 0, (&mut *tld).stats);
    }

    if !p.is_null() {
        mi_stat_increase(&mut (&mut *(&mut *tld).stats).reserved, size);
    }

    let alloc_align = _mi_os_page_size();
    let probable_size = MI_SEGMENT_SIZE;

    if (&mut *tld).mmap_previous as usize > p as usize {
        (&mut *tld).mmap_next_probable =
            _mi_align_down(p as usize - probable_size as usize, alloc_align);
    } else {
        (&mut *tld).mmap_next_probable =
            _mi_align_up(p as usize + probable_size as usize, alloc_align);
    }
    (&mut *tld).mmap_previous = p as _;
    p
}

pub unsafe fn os_pool_alloc(_size: usize, _alignment: usize, _tld: *mut mi_os_tld_t) -> *mut u8 {
    null_mut()
}

pub unsafe fn mi_os_alloc_aligned_ensured(
    size: usize,
    alignment: usize,
    trie: usize,
    stats: *mut mi_stats_t,
) -> *mut u8 {
    if trie >= 3 {
        return null_mut();
    }
    let alloc_size = size + alignment;
    if alloc_size < size {
        return null_mut();
    }

    let p = mi_mmap(null_mut(), alloc_size, 0, stats);
    if p.is_null() {
        return null_mut();
    }
    let aligned_p = mi_align_up_ptr(p, alignment);
    let pre_size = aligned_p as usize - p as usize;
    let mid_size = _mi_align_up(size, _mi_os_page_size());
    let post_size = alloc_size - pre_size - mid_size;
    if pre_size > 0 {
        mi_munmap(p, pre_size);
    }
    if post_size > 0 {
        mi_munmap(aligned_p.add(mid_size), pre_size);
    }

    aligned_p
}
